import { LayerGroup } from './LayerGroup';
import { FeatureGroup } from './FeatureGroup';
import * as Util from '../core/Util';
import { Marker } from './marker/Marker';
import { Circle } from './vector/Circle';
import { CircleMarker } from './vector/CircleMarker';
import { Polyline } from './vector/Polyline';
import { Polygon } from './vector/Polygon';
import { LatLng } from '../geo/LatLng';
import * as LineUtil from '../geometry/LineUtil';
import { toLatLng } from '../geo/LatLng';

/*
 * @class GeoJSON
 * @aka L.GeoJSON
 * @inherits FeatureGroup
 *
 * Represents a GeoJSON object or an array of GeoJSON objects. Allows you to parse
 * GeoJSON data and display it on the map. Extends `FeatureGroup`.
 *
 * @example
 *
 * ```js
 * L.geoJSON(data, {
 * 	style: function (feature) {
 * 		return {color: feature.properties.color};
 * 	}
 * }).bindPopup(function (layer) {
 * 	return layer.feature.properties.description;
 * }).addTo(map);
 * ```
 */

export var GeoJSON = FeatureGroup.extend({
  /* @section
   * @aka GeoJSON options
   *
   * @option pointToLayer: Function = *
   * A `Function` defining how GeoJSON points spawn Leaflet layers. It is internally
   * called when data is added, passing the GeoJSON point feature and its `LatLng`.
   * The default is to spawn a default `Marker`:
   * ```js
   * function(geoJsonPoint, latlng) {
   * 	return L.marker(latlng);
   * }
   * ```
   *
   * @option style: Function = *
   * A `Function` defining the `Path options` for styling GeoJSON lines and polygons,
   * called internally when data is added.
   * The default value is to not override any defaults:
   * ```js
   * function (geoJsonFeature) {
   * 	return {}
   * }
   * ```
   *
   * @option onEachFeature: Function = *
   * A `Function` that will be called once for each created `Feature`, after it has
   * been created and styled. Useful for attaching events and popups to features.
   * The default is to do nothing with the newly created layers:
   * ```js
   * function (feature, layer) {}
   * ```
   *
   * @option filter: Function = *
   * A `Function` that will be used to decide whether to include a feature or not.
   * The default is to include all features:
   * ```js
   * function (geoJsonFeature) {
   * 	return true;
   * }
   * ```
   * Note: dynamically changing the `filter` option will have effect only on newly
   * added data. It will _not_ re-evaluate already included features.
   *
   * @option coordsToLatLng: Function = *
   * A `Function` that will be used for converting GeoJSON coordinates to `LatLng`s.
   * The default is the `coordsToLatLng` static method.
   *
   * @option markersInheritOptions: Boolean = false
   * Whether default Markers for "Point" type Features inherit from group options.
   */

  initialize: function (geojson, options) {
    Util.setOptions(this, options);

    this._layers = {};

    if (geojson) {
      this.addData(geojson);
    }
  },

  // @method addData( <GeoJSON> data ): this
  // Adds a GeoJSON object to the layer.
  addData: function (geojson) {
    var features = Util.isArray(geojson) ? geojson : geojson.features,
      i,
      len,
      feature;

    if (features) {
      for (i = 0, len = features.length; i < len; i++) {
        // only add this if geometry or geometries are set and not null
        feature = features[i];
        if (
          feature.geometries ||
          feature.geometry ||
          feature.features ||
          feature.coordinates
        ) {
          this.addData(feature);
        }
      }
      return this;
    }

    var options = this.options;

    if (options.filter && !options.filter(geojson)) {
      return this;
    }

    var layer = geometryToLayer(geojson, options);
    if (!layer) {
      return this;
    }
    layer.feature = asFeature(geojson);

    layer.defaultOptions = layer.options;
    this.resetStyle(layer);

    if (options.onEachFeature) {
      options.onEachFeature(geojson, layer);
    }

    return this.addLayer(layer);
  },

  // @method resetStyle( <Path> layer? ): this
  // Resets the given vector layer's style to the original GeoJSON style, useful for resetting style after hover events.
  // If `layer` is omitted, the style of all features in the current layer is reset.
  resetStyle: function (layer) {
    if (layer === undefined) {
      return this.eachLayer(this.resetStyle, this);
    }
    // reset any custom styles
    layer.options = Util.extend({}, layer.defaultOptions);
    this._setLayerStyle(layer, this.options.style);
    return this;
  },

  // @method setStyle( <Function> style ): this
  // Changes styles of GeoJSON vector layers with the given style function.
  setStyle: function (style) {
    return this.eachLayer(function (layer) {
      this._setLayerStyle(layer, style);
    }, this);
  },

  _setLayerStyle: function (layer, style) {
    if (layer.setStyle) {
      if (typeof style === 'function') {
        style = style(layer.feature);
      }
      layer.setStyle(style);
    }
  },
});

// @section
// There are several static functions which can be called without instantiating L.GeoJSON:

// @function geometryToLayer(featureData: Object, options?: GeoJSON options): Layer
// Creates a `Layer` from a given GeoJSON feature. Can use a custom
// [`pointToLayer`](#geojson-pointtolayer) and/or [`coordsToLatLng`](#geojson-coordstolatlng)
// functions if provided as options.
export function geometryToLayer(geojson, options) {
  var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
    coords = geometry ? geometry.coordinates : null,
    layers = [],
    pointToLayer = options && options.pointToLayer,
    _coordsToLatLng = (options && options.coordsToLatLng) || coordsToLatLng,
    latlng,
    latlngs,
    i,
    len;

  if (!coords && !geometry) {
    return null;
  }

  switch (geometry.type) {
    case 'Point':
      latlng = _coordsToLatLng(coords);
      return _pointToLayer(pointToLayer, geojson, latlng, options);

    case 'MultiPoint':
      for (i = 0, len = coords.length; i < len; i++) {
        latlng = _coordsToLatLng(coords[i]);
        layers.push(_pointToLayer(pointToLayer, geojson, latlng, options));
      }
      return new FeatureGroup(layers);

    case 'LineString':
    case 'MultiLineString':
      latlngs = coordsToLatLngs(
        coords,
        geometry.type === 'LineString' ? 0 : 1,
        _coordsToLatLng,
      );
      return new Polyline(latlngs, options);

    case 'Polygon':
    case 'MultiPolygon':
      latlngs = coordsToLatLngs(
        coords,
        geometry.type === 'Polygon' ? 1 : 2,
        _coordsToLatLng,
      );
      return new Polygon(latlngs, options);

    case 'GeometryCollection':
      for (i = 0, len = geometry.geometries.length; i < len; i++) {
        var geoLayer = geometryToLayer(
          {
            geometry: geometry.geometries[i],
            type: 'Feature',
            properties: geojson.properties,
          },
          options,
        );

        if (geoLayer) {
          layers.push(geoLayer);
        }
      }
      return new FeatureGroup(layers);

    case 'FeatureCollection':
      for (i = 0, len = geometry.features.length; i < len; i++) {
        var featureLayer = geometryToLayer(geometry.features[i], options);

        if (featureLayer) {
          layers.push(featureLayer);
        }
      }
      return new FeatureGroup(layers);

    default:
      throw new Error('Invalid GeoJSON object.');
  }
}

function _pointToLayer(pointToLayerFn, geojson, latlng, options) {
  return pointToLayerFn
    ? pointToLayerFn(geojson, latlng)
    : new Marker(latlng, options && options.markersInheritOptions && options);
}

// @function coordsToLatLng(coords: Array): LatLng
// Creates a `LatLng` object from an array of 2 numbers (longitude, latitude)
// or 3 numbers (longitude, latitude, altitude) used in GeoJSON for points.
export function coordsToLatLng(coords) {
  return new LatLng(coords[1], coords[0], coords[2]);
}

// @function coordsToLatLngs(coords: Array, levelsDeep?: Number, coordsToLatLng?: Function): Array
// Creates a multidimensional array of `LatLng`s from a GeoJSON coordinates array.
// `levelsDeep` specifies the nesting level (0 is for an array of points, 1 for an array of arrays of points, etc., 0 by default).
// Can use a custom [`coordsToLatLng`](#geojson-coordstolatlng) function.
export function coordsToLatLngs(coords, levelsDeep, _coordsToLatLng) {
  var latlngs = [];

  for (var i = 0, len = coords.length, latlng; i < len; i++) {
    latlng = levelsDeep
      ? coordsToLatLngs(coords[i], levelsDeep - 1, _coordsToLatLng)
      : (_coordsToLatLng || coordsToLatLng)(coords[i]);

    latlngs.push(latlng);
  }

  return latlngs;
}

// @function latLngToCoords(latlng: LatLng, precision?: Number|false): Array
// Reverse of [`coordsToLatLng`](#geojson-coordstolatlng)
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function.
export function latLngToCoords(latlng, precision) {
  latlng = toLatLng(latlng);
  return latlng.alt !== undefined
    ? [
        Util.formatNum(latlng.lng, precision),
        Util.formatNum(latlng.lat, precision),
        Util.formatNum(latlng.alt, precision),
      ]
    : [
        Util.formatNum(latlng.lng, precision),
        Util.formatNum(latlng.lat, precision),
      ];
}

// @function latLngsToCoords(latlngs: Array, levelsDeep?: Number, closed?: Boolean, precision?: Number|false): Array
// Reverse of [`coordsToLatLngs`](#geojson-coordstolatlngs)
// `closed` determines whether the first point should be appended to the end of the array to close the feature, only used when `levelsDeep` is 0. False by default.
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function.
export function latLngsToCoords(latlngs, levelsDeep, closed, precision) {
  var coords = [];

  for (var i = 0, len = latlngs.length; i < len; i++) {
    // Check for flat arrays required to ensure unbalanced arrays are correctly converted in recursion
    coords.push(
      levelsDeep
        ? latLngsToCoords(
            latlngs[i],
            LineUtil.isFlat(latlngs[i]) ? 0 : levelsDeep - 1,
            closed,
            precision,
          )
        : latLngToCoords(latlngs[i], precision),
    );
  }

  if (!levelsDeep && closed && coords.length > 0) {
    coords.push(coords[0].slice());
  }

  return coords;
}

export function getFeature(layer, newGeometry) {
  return layer.feature
    ? Util.extend({}, layer.feature, { geometry: newGeometry })
    : asFeature(newGeometry);
}

// @function asFeature(geojson: Object): Object
// Normalize GeoJSON geometries/features into GeoJSON features.
export function asFeature(geojson) {
  if (geojson.type === 'Feature' || geojson.type === 'FeatureCollection') {
    return geojson;
  }

  return {
    type: 'Feature',
    properties: {},
    geometry: geojson,
  };
}

var PointToGeoJSON = {
  toGeoJSON: function (precision) {
    return getFeature(this, {
      type: 'Point',
      coordinates: latLngToCoords(this.getLatLng(), precision),
    });
  },
};

// @namespace Marker
// @section Other methods
// @method toGeoJSON(precision?: Number|false): Object
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the marker (as a GeoJSON `Point` Feature).
Marker.include(PointToGeoJSON);

// @namespace CircleMarker
// @method toGeoJSON(precision?: Number|false): Object
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the circle marker (as a GeoJSON `Point` Feature).
Circle.include(PointToGeoJSON);
CircleMarker.include(PointToGeoJSON);

// @namespace Polyline
// @method toGeoJSON(precision?: Number|false): Object
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polyline (as a GeoJSON `LineString` or `MultiLineString` Feature).
Polyline.include({
  toGeoJSON: function (precision) {
    var multi = !LineUtil.isFlat(this._latlngs);

    var coords = latLngsToCoords(
      this._latlngs,
      multi ? 1 : 0,
      false,
      precision,
    );

    return getFeature(this, {
      type: (multi ? 'Multi' : '') + 'LineString',
      coordinates: coords,
    });
  },
});

// @namespace Polygon
// @method toGeoJSON(precision?: Number|false): Object
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polygon (as a GeoJSON `Polygon` or `MultiPolygon` Feature).
Polygon.include({
  toGeoJSON: function (precision) {
    var holes = !LineUtil.isFlat(this._latlngs),
      multi = holes && !LineUtil.isFlat(this._latlngs[0]);

    var coords = latLngsToCoords(
      this._latlngs,
      multi ? 2 : holes ? 1 : 0,
      true,
      precision,
    );

    if (!holes) {
      coords = [coords];
    }

    return getFeature(this, {
      type: (multi ? 'Multi' : '') + 'Polygon',
      coordinates: coords,
    });
  },
});

// @namespace LayerGroup
LayerGroup.include({
  toMultiPoint: function (precision) {
    var coords = [];

    this.eachLayer(function (layer) {
      coords.push(layer.toGeoJSON(precision).geometry.coordinates);
    });

    return getFeature(this, {
      type: 'MultiPoint',
      coordinates: coords,
    });
  },

  // @method toGeoJSON(precision?: Number|false): Object
  // Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
  // Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the layer group (as a GeoJSON `FeatureCollection`, `GeometryCollection`, or `MultiPoint`).
  toGeoJSON: function (precision) {
    var type =
      this.feature && this.feature.geometry && this.feature.geometry.type;

    if (type === 'MultiPoint') {
      return this.toMultiPoint(precision);
    }

    var isGeometryCollection = type === 'GeometryCollection',
      jsons = [];

    this.eachLayer(function (layer) {
      if (layer.toGeoJSON) {
        var json = layer.toGeoJSON(precision);
        if (isGeometryCollection) {
          jsons.push(json.geometry);
        } else {
          var feature = asFeature(json);
          // Squash nested feature collections
          if (feature.type === 'FeatureCollection') {
            jsons.push.apply(jsons, feature.features);
          } else {
            jsons.push(feature);
          }
        }
      }
    });

    if (isGeometryCollection) {
      return getFeature(this, {
        geometries: jsons,
        type: 'GeometryCollection',
      });
    }

    return {
      type: 'FeatureCollection',
      features: jsons,
    };
  },
});

// @namespace GeoJSON
// @factory L.geoJSON(geojson?: Object, options?: GeoJSON options)
// Creates a GeoJSON layer. Optionally accepts an object in
// [GeoJSON format](https://tools.ietf.org/html/rfc7946) to display on the map
// (you can alternatively add it later with `addData` method) and an `options` object.
export function geoJSON(geojson, options) {
  return new GeoJSON(geojson, options);
}

// Backward compatibility.
export var geoJson = geoJSON;
